#include "opencl_runtime.h"
#include <dlfcn.h>
#ifdef SHARING_MEM_WITH_OPENGL
#include <EGL/egl.h>
#endif
#include <map>

namespace PanNN {
namespace OpenCL {

static std::mutex g_initMtx;
OpenCLRuntime::InitState OpenCLRuntime::m_initState = UnInit;

std::map<cl_int, std::string> errorInfos = {
    {CL_SUCCESS, "Success"},
    {CL_DEVICE_NOT_FOUND, "Device not found"},
    {CL_DEVICE_NOT_AVAILABLE, "Device not available"},
    {CL_COMPILER_NOT_AVAILABLE, "Compiler not available"},
    {CL_MEM_OBJECT_ALLOCATION_FAILURE, "Memory object allocation failure"},
    {CL_OUT_OF_RESOURCES, "Out of resources"},
    {CL_OUT_OF_HOST_MEMORY, "Out of host memory"},
    {CL_PROFILING_INFO_NOT_AVAILABLE, "Profiling information not available"},
    {CL_MEM_COPY_OVERLAP, "Memory copy overlap"},
    {CL_IMAGE_FORMAT_MISMATCH, "Image format mismatch"},
    {CL_IMAGE_FORMAT_NOT_SUPPORTED, "Image format not supported"},
    {CL_BUILD_PROGRAM_FAILURE, "Build program failure"},
    {CL_MAP_FAILURE, "Mapping failure"},
    {CL_MISALIGNED_SUB_BUFFER_OFFSET, "Misaligned sub-buffer offset"},
    {CL_EXEC_STATUS_ERROR_FOR_EVENTS_IN_WAIT_LIST, "Execution status error for events in wait list"},
    {CL_COMPILE_PROGRAM_FAILURE, "Compile program failure"},
    {CL_LINKER_NOT_AVAILABLE, "Linker not available"},
    {CL_LINK_PROGRAM_FAILURE, "Link program failure"},
    {CL_DEVICE_PARTITION_FAILED, "Device partition failed"},
    {CL_KERNEL_ARG_INFO_NOT_AVAILABLE, "Kernel argument information not available"},
    {CL_INVALID_VALUE, "Invalid value"},
    {CL_INVALID_DEVICE_TYPE, "Invalid device type"},
    {CL_INVALID_PLATFORM, "Invalid platform"},
    {CL_INVALID_DEVICE, "Invalid device"},
    {CL_INVALID_CONTEXT, "Invalid context"},
    {CL_INVALID_QUEUE_PROPERTIES, "Invalid queue properties"},
    {CL_INVALID_COMMAND_QUEUE, "Invalid command queue"},
    {CL_INVALID_HOST_PTR, "Invalid host pointer"},
    {CL_INVALID_MEM_OBJECT, "Invalid memory object"},
    {CL_INVALID_IMAGE_FORMAT_DESCRIPTOR, "Invalid image format descriptor"},
    {CL_INVALID_IMAGE_SIZE, "Invalid image size"},
    {CL_INVALID_SAMPLER, "Invalid sampler"},
    {CL_INVALID_BINARY, "Invalid binary"},
    {CL_INVALID_BUILD_OPTIONS, "Invalid build options"},
    {CL_INVALID_PROGRAM, "Invalid program"},
    {CL_INVALID_PROGRAM_EXECUTABLE, "Invalid program executable"},
    {CL_INVALID_KERNEL_NAME, "Invalid kernel name"},
    {CL_INVALID_KERNEL_DEFINITION, "Invalid kernel definition"},
    {CL_INVALID_KERNEL, "Invalid kernel"},
    {CL_INVALID_ARG_INDEX, "Invalid argument index"},
    {CL_INVALID_ARG_VALUE, "Invalid argument value"},
    {CL_INVALID_ARG_SIZE, "Invalid argument size"},
    {CL_INVALID_KERNEL_ARGS, "Invalid kernel arguments"},
    {CL_INVALID_WORK_DIMENSION, "Invalid work dimension"},
    {CL_INVALID_WORK_GROUP_SIZE, "Invalid work group size"},
    {CL_INVALID_WORK_ITEM_SIZE, "Invalid work item size"},
    {CL_INVALID_GLOBAL_OFFSET, "Invalid global offset"},
    {CL_INVALID_EVENT_WAIT_LIST, "Invalid event wait list"},
    {CL_INVALID_EVENT, "Invalid event"},
    {CL_INVALID_OPERATION, "Invalid operation"},
    {CL_INVALID_GL_OBJECT, "Invalid GL object"},
    {CL_INVALID_BUFFER_SIZE, "Invalid buffer size"},
    {CL_INVALID_MIP_LEVEL, "Invalid mip-level"},
    {CL_INVALID_GLOBAL_WORK_SIZE, "Invalid global work size"},
    {CL_INVALID_PROPERTY, "Invalid property"},
    {CL_INVALID_IMAGE_DESCRIPTOR, "Invalid image descriptor"},
    {CL_INVALID_COMPILER_OPTIONS, "Invalid compiler options"},
    {CL_INVALID_LINKER_OPTIONS, "Invalid linker options"},
    {CL_INVALID_DEVICE_PARTITION_COUNT, "Invalid device partition count"},
    {CL_INVALID_PIPE_SIZE, "Invalid pipe size"},
    {CL_INVALID_DEVICE_QUEUE, "Invalid device queue"},
    {CL_INVALID_GL_SHAREGROUP_REFERENCE_KHR, "Invalid GL share group reference KHR"}};

std::string OpenCLRuntime::CLErrorCode(cl_int errorCode) {
    auto it = errorInfos.find(errorCode);
    if (it == errorInfos.end()) {
        return "Unknown OpenCL error code";
    } else {
        return it->second;
    }
}

OpenCLRuntime::OpenCLRuntime() {}
OpenCLRuntime::~OpenCLRuntime() {}

// get gpu divce type
OpenCLRuntime::GpuInfo OpenCLRuntime::ParseGpuInfo(std::string deviceName, std::string deviceVersion) {
    GpuInfo info;
    if (deviceName == "QUALCOMM Adreno(TM)") {
        info.m_type = GpuInfo::GpuType::ADRENO;
    } else if (deviceName.find("Mali") != std::string::npos) {
        info.m_type = GpuInfo::GpuType::MALI;
        // Mali type MALI-G or MALI_T
        if (deviceName.find("Mali-G") != std::string::npos) {
            info.m_type = GpuInfo::GpuType::MALI_G;
        } else if (deviceName.find("Mali-T") != std::string::npos) {
            info.m_type = GpuInfo::GpuType::MALI_T;
        }
    }
    return info;
}

ErrorCode OpenCLRuntime::InitGPUDevice(std::vector<cl::Platform> *platforms) {
    PanNN_ASSERT(platforms);
    // search GPU
    std::vector<cl::Device> devices;
    cl_int clRet;
    for (auto &platform : *platforms) {
        std::string platformName;
        clRet = platform.getInfo(CL_PLATFORM_NAME, &platformName);
        if (clRet != CL_SUCCESS) {
            PNN_LOG(WARNING) << CLErrorCode(clRet);
        }
        clRet = platform.getDevices(CL_DEVICE_TYPE_GPU, &devices);
        if (clRet != CL_SUCCESS) {
            PNN_LOG(WARNING) << CLErrorCode(clRet);
        }
        PNN_LOG(INFO) << "Platform (" << platformName << ") has " << devices.size() << " GPUs";

        if (!devices.empty()) {
            std::string deviceName = devices[0].getInfo<CL_DEVICE_NAME>();
            PNN_LOG(INFO) << "Find GPU: " << deviceName.c_str();
            cl::Platform::setDefault(platform);
            break;
        }
    }

    // not found, return error code.
    if (devices.empty()) {
        PNN_LOG(ERROR) << "OpenCL Device not found!";
        return CL_RUNTIME_INIT_ERR;
    }

    m_device = new (std::nothrow) cl::Device();
    if (m_device == nullptr) {
        PNN_LOG(ERROR) << "Create OpenCL device failed!";
        return CL_RUNTIME_INIT_ERR;
    }
    *m_device = devices[0];

    m_maxWorkItemSizes = m_device->getInfo<CL_DEVICE_MAX_WORK_ITEM_SIZES>();
    m_maxWorkGroupSize = m_maxWorkItemSizes[0];
    const std::string deviceName = m_device->getInfo<CL_DEVICE_NAME>();
    const std::string deviceVersion = m_device->getInfo<CL_DEVICE_VERSION>();
    const std::string openCLVersion = m_device->getInfo<CL_DEVICE_OPENCL_C_VERSION>();
    clGetDeviceInfo((*m_device)(), CL_DEVICE_IMAGE_PITCH_ALIGNMENT, sizeof(cl_uint), &m_imagePitchAlign, nullptr);
    PNN_LOG(INFO) << "Device name:\t" << deviceName;
    PNN_LOG(INFO) << "Opencl version:\t" << deviceVersion;
    PNN_LOG(INFO) << "Image pitch alignment:\t" << m_imagePitchAlign;
    PNN_LOG(INFO) << "Highest OpenCL c version:\t" << openCLVersion;
    PNN_LOG(INFO) << "Max work item size:\t" << m_maxWorkItemSizes[0] << " : " << m_maxWorkItemSizes[1] << " : "
                  << m_maxWorkItemSizes[2];

    m_gpuInfo = ParseGpuInfo(deviceName, deviceVersion);
    // get cache size, compute units and frequency.
    clRet = m_device->getInfo(CL_DEVICE_GLOBAL_MEM_CACHE_SIZE, &m_globalMemeryCachesize);
    if (clRet != CL_SUCCESS) {
        PNN_LOG(WARNING) << CLErrorCode(clRet);
    }
    clRet = m_device->getInfo(CL_DEVICE_MAX_COMPUTE_UNITS, &m_computeUnits);
    if (clRet != CL_SUCCESS) {
        PNN_LOG(WARNING) << CLErrorCode(clRet);
    }
    clRet = m_device->getInfo(CL_DEVICE_MAX_CLOCK_FREQUENCY, &m_maxFreq);
    if (clRet != CL_SUCCESS) {
        PNN_LOG(WARNING) << CLErrorCode(clRet);
    }
    cl_device_fp_config fp_config;
    auto success = m_device->getInfo(CL_DEVICE_HALF_FP_CONFIG, &fp_config);
    m_supportFP16 = CL_SUCCESS == success && fp_config > 0;

    clRet = m_device->getInfo(CL_DEVICE_SVM_CAPABILITIES, &m_svmCapabilities);
    if (clRet != CL_SUCCESS || m_svmCapabilities == 0) {
        m_svmCapabilities = 0;
        PNN_LOG(INFO) << "SVM capalibilties: NONE";
    } else {
        if (m_svmCapabilities & CL_DEVICE_SVM_FINE_GRAIN_BUFFER) {
            PNN_LOG(INFO) << "SVM capalibilties: SVM_FINE_GRAIN_BUFFER";
        }
        if (m_svmCapabilities & CL_DEVICE_SVM_COARSE_GRAIN_BUFFER) {
            PNN_LOG(INFO) << "SVM capalibilties: SVM_COARSE_GRAIN_BUFFER";
        }
        if (m_svmCapabilities & CL_DEVICE_SVM_FINE_GRAIN_SYSTEM) {
            PNN_LOG(INFO) << "SVM capalibilties: SVM_COARSE_GRAIN_SYSTEM";
        }
        if (m_svmCapabilities & CL_DEVICE_SVM_ATOMICS) {
            PNN_LOG(INFO) << "SVM capalibilties: SVM_ATOMICS";
        }
    }
    m_globalMemerySize = m_device->getInfo<CL_DEVICE_GLOBAL_MEM_SIZE>();
    m_maxAllocSize = m_device->getInfo<CL_DEVICE_MAX_MEM_ALLOC_SIZE>();
    m_maxImage2dWidth = m_device->getInfo<CL_DEVICE_IMAGE2D_MAX_WIDTH>();
    m_maxImage2dHeight = m_device->getInfo<CL_DEVICE_IMAGE2D_MAX_HEIGHT>();
    m_supportedExtensions = std::string(m_device->getInfo<CL_DEVICE_EXTENSIONS>());
    m_cacheLineSize = m_device->getInfo<CL_DEVICE_GLOBAL_MEM_CACHELINE_SIZE>();
    PNN_LOG(INFO) << "Address space bits: " << m_device->getInfo<CL_DEVICE_ADDRESS_BITS>();
    PNN_LOG(INFO) << "Global Mem Size: " << m_globalMemerySize;
    PNN_LOG(INFO) << "Global Mem Cache Size: " << m_globalMemeryCachesize;
    PNN_LOG(INFO) << "Max Alloc Size: " << m_maxAllocSize;
    PNN_LOG(INFO) << "Compute Unit: " << m_computeUnits;
    PNN_LOG(INFO) << "Clock Frequency: " << m_maxFreq << " MHz";
    return NONE_ERR;
}

ErrorCode OpenCLRuntime::InitQueue(std::vector<cl::Platform> *platforms) {
    PanNN_ASSERT(platforms);
    cl_int clRet = 0;
#if defined(SHARING_MEM_WITH_OPENGL) && defined(CL_HPP_TARGET_OPENCL_VERSION) && (CL_HPP_TARGET_OPENCL_VERSION >= 120)
    // create context from glcontext
    PNN_LOG(INFO) << "Create special opencl context to share with OpenGL";
    cl_context_properties context_prop[] = {CL_GL_CONTEXT_KHR, (cl_context_properties)eglGetCurrentContext(),
                                            CL_EGL_DISPLAY_KHR, (cl_context_properties)eglGetCurrentDisplay(), 0};
    m_context =
        new (std::nothrow) cl::Context(std::vector<cl::Device>{*m_device}, context_prop, nullptr, nullptr, &clRet);
    if (m_context == nullptr || clRet != CL_SUCCESS) {
        PNN_LOG(ERROR) << "Create special OpenCL context failed, Create common OpenCL context then.";
        if (m_context != nullptr) {
            delete m_context;
        }
        m_context =
            new (std::nothrow) cl::Context(std::vector<cl::Device>{*m_device}, nullptr, nullptr, nullptr, &clRet);
        if (m_context == nullptr) {
            delete m_device;
            PNN_LOG(ERROR) << "Create OpenCL context failed!";
            return CL_RUNTIME_INIT_ERR;
        }
    }
#else
    PNN_LOG(INFO) << "Create common opencl context";
#ifdef Debug
    std::vector<cl_context_properties> ctx_properties = {CL_CONTEXT_PLATFORM,
                                                         (cl_context_properties)(*platforms)[0](),
                                                         CL_PRINTF_CALLBACK_ARM,
                                                         (cl_context_properties)printf_callback,
                                                         CL_PRINTF_BUFFERSIZE_ARM,
                                                         0x1000000,
                                                         0};
    m_context = new (std::nothrow)
        cl::Context(std::vector<cl::Device>{*m_device}, ctx_properties.data(), nullptr, nullptr, &clRet);
    if (m_context == nullptr || clRet != CL_SUCCESS) {
        m_context =
            new (std::nothrow) cl::Context(std::vector<cl::Device>{*m_device}, nullptr, nullptr, nullptr, &clRet);
    }
#else
    m_context = new (std::nothrow) cl::Context(std::vector<cl::Device>{*m_device}, nullptr, nullptr, nullptr, &clRet);
#endif
#endif
    if (m_context == nullptr || clRet != CL_SUCCESS) {
        delete m_device;
        m_device = nullptr;
        PNN_LOG(ERROR) << "Context create failed: " << CLErrorCode(clRet);
        return CL_RUNTIME_INIT_ERR;
    }

    m_defaultCmdQueue = new (std::nothrow) cl::CommandQueue(*m_context, *m_device, 0, &clRet);
    if (m_defaultCmdQueue == nullptr || clRet != CL_SUCCESS) {
        delete m_device;
        delete m_context;
        m_device = nullptr;
        m_context = nullptr;
        PNN_LOG(ERROR) << "Command Queue create failed: " << CLErrorCode(clRet);
        return CL_RUNTIME_INIT_ERR;
    }

    m_profilingCmdQueue = new (std::nothrow) cl::CommandQueue(*m_context, *m_device, CL_QUEUE_PROFILING_ENABLE, &clRet);
    if (m_profilingCmdQueue == nullptr || clRet != CL_SUCCESS) {
        delete m_device;
        delete m_context;
        delete m_defaultCmdQueue;
        m_device = nullptr;
        m_context = nullptr;
        m_defaultCmdQueue = nullptr;
        PNN_LOG(ERROR) << "Profiling command Queue create failed: " << CLErrorCode(clRet);
        return CL_RUNTIME_INIT_ERR;
    }
    return NONE_ERR;
}

ErrorCode OpenCLRuntime::Init() {
    std::unique_lock<std::mutex> lck(g_initMtx);
    if (m_initState == InitSuccess) {
        return NONE_ERR;
    } else if (m_initState == InitFailed) {
        return CL_RUNTIME_INIT_ERR;
    }
    m_initState = InitFailed;

    PNN_LOG(INFO) << "OpenCL version: CL_TARGET_OPENCL_VERSION " << CL_TARGET_OPENCL_VERSION;
    PNN_LOG(INFO) << "CL_HPP_TARGET_OPENCL_VERSION " << CL_HPP_TARGET_OPENCL_VERSION;
    PNN_LOG(INFO) << "CL_HPP_MINIMUM_OPENCL_VERSION " << CL_HPP_MINIMUM_OPENCL_VERSION;

#ifdef USE_OPENCL_WRAPPER
    if (!LoadOpenCLLibrary(&m_handle)) {
        PNN_LOG(ERROR) << "Load OpenCL symbols failed!";
        return CL_RUNTIME_INIT_ERR;
    }
#endif  // USE_OPENCL_WRAPPER
    std::vector<cl::Platform> platforms;
    cl_int clRet = cl::Platform::get(&platforms);
    if (platforms.empty()) {
        PNN_LOG(ERROR) << "OpenCL Platform not found!" << CLErrorCode(clRet);
        return CL_RUNTIME_INIT_ERR;
    }
    auto pnnRet = InitGPUDevice(&platforms);
    if (pnnRet != NONE_ERR) {
        return pnnRet;
    }

    // only support mali device.
    if (m_gpuInfo.m_type == GpuInfo::MALI || m_gpuInfo.m_type == GpuInfo::MALI_T ||
        m_gpuInfo.m_type == GpuInfo::MALI_G) {
#ifdef USE_OPENCL_WRAPPER
        clImportMemoryARM = reinterpret_cast<clImportMemoryARMFunc>(dlsym(m_handle, "clImportMemoryARM"));
        if (clImportMemoryARM == nullptr) {
            PNN_LOG(ERROR) << "load func (clImportMemoryARM) failed!";
            dlclose(m_handle);
            return CL_RUNTIME_INIT_ERR;
        }
#endif  // USE_OPENCL_WRAPPER
    }

    pnnRet = InitQueue(&platforms);
    if (pnnRet != NONE_ERR) {
        return pnnRet;
    }

    // allocator_ = std::make_shared<OpenCLAllocator>(this);
    // if (allocator_ == nullptr) {
    //     delete m_device;
    //     delete m_context;
    //     delete m_defaultCmdQueue;
    //     delete m_profilingCmdQueue;
    //     m_device = nullptr;
    //     m_context = nullptr;
    //     m_defaultCmdQueue = nullptr;
    //     m_profilingCmdQueue = nullptr;
    //     PNN_LOG(ERROR) << "Command OpenCL allocator failed!";
    //     return CL_RUNTIME_INIT_ERR;
    // }
    // LoadCache();
    // init_state_ = InitSuccess;
    // PNN_LOG(INFO) << "OpenCLRuntime init done!";
    // return NONE_ERR;
    return NONE_ERR;
}
}  // namespace OpenCL
}  // namespace PanNN
